package com.ease.framework {

import com.ease.util.MixedUtil;
import com.ease.util.ReflectionUtil;
import com.ease.util.TaggedMemberInfo;
import com.ease.util.TaggedMethodInfo;
import com.ease.util.TaggedPropertyInfo;
import com.ease.util.logging.LogManager;
import com.ease.framework.priv.ResponseInfo;

import flash.events.Event;

import mx.logging.ILogger;
import mx.utils.StringUtil;

/**
 * @private
 *
 * Represents a delegate for an event listener. It is used to validate or filter the
 * invocations of the listener, or to transform the event into some other form of
 * parameters for the listener method based on what it expects.
 *
 * <p>If an event contains an error and the target of the event notification has a generic
 * error handler (i.e., a method that is tagged as [ErrorHandler]), then that method is
 * used to report the error. In such a case, if the event/response handler takes only 2
 * parameters, i.e., does not take error as a parameter, the event/response handler is
 * skipped. In other words, if an event contains an error and the target has not
 * subscribed for the error parameter, then partial results are ignored.
 *
 * <p>A response handler can receive either the response event itself, or between 2 to 4
 * of these arguments: the properties of the response event that are labeled as [Request],
 * [Result], [Error] and [ResultInfo]. The error and result-info properties are optional.
 *
 * <p>A response event is dropped if it doesn't have a [Request] property, or if the
 * requestor's [Context] properties have changed since making the request.
 */
internal class EventFunctor
{
   private static const _logger:ILogger = LogManager.getLogger( EventBus );
   /**
    * Either the whole response event is delivered (to a response handler), or
    * [request, result] is delivered.
    */
   private static const RESPONSE_EVENT_ARG_INDEX:int = 0;
   private static const REQUEST_EVENT_ARG_INDEX:int = 0;
   private static const RESULT_ARG_INDEX:int = 1;
   private static const ERROR_ARG_INDEX:int = 2;
   private static const RESPONSE_INFO_SENTINEL:ResponseInfo = new ResponseInfo();
   private var _targetErrorHandlers:Array/*Function*/ = [];

   //-------------------------------------------------------
   // Property: target

   private var _target:Object;

   /** The owner of the event listener/callback/handler. */
   public function get target():Object {
      return _target;
   }

   /** @private */
   public function set target( value:Object ):void {
      _target = value;

      var errHandlerInfos:Array/*TaggedMethodInfo*/ =
            ReflectionUtil.getTaggedMethods( _target, Constants.TAG_ERROR_HANDLER );
      for each (var methodInfo:TaggedMethodInfo in errHandlerInfos) {
         var errorHandler:Function = _target[methodInfo.name];
         _targetErrorHandlers.push( errorHandler );
      }
   }

   //-------------------------------------------------------
   /** The callback or handler for the event. */
   public var listener:Function;

   /** Metadata for the handler method or property. */
   public var handlerInfo:TaggedMemberInfo;

   /**
    * The pending-request registry that is to be used to lookup information on the events
    * that get dispatched through this functor.
    */
   public var pendingRequestRegistry:PendingRequestRegistry;

   /**
    * Indicates whether the listener is a [ResponseHandler] (as opposed to an
    * [EventHandler]).
    */
   public var isResponseHandler:Boolean = false;

   /**
    * Indicates that if the event is a response to some earlier event, then the listener
    * should be invoked only if the original event was raised by the same object (i.e.,
    * the target of the listener). The default value is true.
    */
   public var isReflexiveOnly:Boolean = true;

   /**
    * This method acts as the delegate for the listener (for the purposes of event
    * notifications).
    */
   public final function onEvent( event:Event ):void {
      if (this.handlerInfo == null) {
         return;
      }
      var responseInfo:ResponseInfo =
            this.pendingRequestRegistry.getResponseInfo( event );
      if (responseInfo == null) {
         // Create a "sentinel" (or empty) info object.
         responseInfo = RESPONSE_INFO_SENTINEL;
      }
      if (isDiscardableResponse( event, responseInfo )) {
         return;
      }
      var argVals:Array/*?*/ = getListenerInvocationArgs( event, responseInfo );
      notifyTarget( event, argVals, responseInfo );
   }

   /**
    * Invokes the error and/or event/response handlers on the target object, given the
    * event and the ordered form and the structured form of the handler arguments. argVals
    * can be null if the response handler is a property. responseInfo applies to the cases
    * where the event is a response (to some request).
    *
    * <p>Invocation is skipped if certain conditions don't meet, e.g., if the event is a
    * response to an obsolete request.
    */
   private function notifyTarget( event:Event, argVals:Array/*?*/,
                                  responseInfo:ResponseInfo ):void {
      var argTypeToValidate:Class;
      var argValToValidate:Object;
      var methodArgTypes:Array/*Class*/ = this.argTypes;
      if (methodArgTypes.length > 0) {
         argTypeToValidate = methodArgTypes[0];
         argValToValidate = argVals[0];
      } else if (this.handlerInfo is TaggedPropertyInfo) {
         argTypeToValidate = TaggedPropertyInfo( this.handlerInfo ).propertyType;
         argValToValidate = responseInfo.result;
      }
      if (argTypeToValidate != null && argValToValidate != null &&
          !(argValToValidate is argTypeToValidate)) {
         // We only log the incompatibility of the primary arg, since that signals that
         // the same event id is being used with different event types, which can happen,
         // e.g., if they come from different modules that were written independently
         // It's not necessarily a bug, hence we don't assert or alert. But an
         // informational message could be helpful.
         var msg:String = StringUtil.substitute(
               "Skipped invocation of {0}.{1} due to type mismatch",
               handlerInfo.declaringType, handlerInfo.name );
         _logger.warn( msg );
         return;
      }
      invokeErrorHandlerIfAny( responseInfo );
      if (! shouldInvokeEventListener( responseInfo )) {
         return;
      }
      if (this.responseHandlerIsProperty) {
         target[this.handlerInfo.name] = responseInfo.result;
         return;
      }
      if (this.argTypes.length == 0) {
         // The event handler has no args.
         listener.apply( _target );
         return;
      }
      if (! MixedUtil.validateObjectTypes( argVals, methodArgTypes )) {
         // Mismatch of the (additional) args, e.g. result or error, merely means that the
         // handler is not supposed to be invoked. (These are args in addition to the
         // first arg which is always an event, mismatch of which is handled earlier by
         // logging a message.) In other words, the types declared on the method are
         // meant to cause filtering (out) of the event notifications (that are not
         // applicable). The client is supposed to know what type of result or error they
         // should expect, or can deal with.
         return;
      }
      listener.apply( _target, argVals );
      onEventDelivered( event, argVals, responseInfo );
   }

   /**
    * Performs any post-processing that may be needed once a listener has been notified
    * of the event, given the list of arguments that were delivered to the handler and the
    * ResponseInfo corresponding to the event.
    */
   private function onEventDelivered( event:Event, argVals:Array/*?*/,
                                      responseInfo:ResponseInfo ):void {
      // If the error was notified, mark it in the response-info as delievered.
      if (responseInfo.error == null) {
         return;
      }
      if (argVals.indexOf( event ) == -1 && argVals.indexOf( responseInfo.error ) == -1) {
         // Neither the event (which includes the error) nor the error itself is found in
         // the args that were delivered.
         return;
      }
      responseInfo.wasErrorDelivered = true;
   }

   private function get responseHandlerIsProperty():Boolean {
      return this.handlerInfo is TaggedPropertyInfo;
   }

   private function invokeErrorHandlerIfAny( responseInfo:ResponseInfo ):void {
      if (_targetErrorHandlers.length == 0) {
         return;
      }
      var error:Error = responseInfo.error;
      if (error == null) {
         return;
      }
      for each (var handler:Function in _targetErrorHandlers) {
         handler( error );
      }
      responseInfo.wasErrorDelivered = true;
   }

   /**
    * Event listener here means either an event/response handler in the method form or a
    * property for the [Result] part of the event.
    */
   private function shouldInvokeEventListener( responseInfo:ResponseInfo ):Boolean {
      // If no error, the event listener has to be invoked (even if the result is null).
      if (responseInfo.error == null) {
         return true;
      }

      // If the listener is expecting more than just the result
      // and/or the error, it has to be invoked. (OR)
      // If the listener has just one argument, in other words it is expecting
      // the complete response event, then it has to be invoked.
      var argCount:int = this.argTypes.length;
      if (argCount == RESPONSE_EVENT_ARG_INDEX + 1 ||
          argCount > ERROR_ARG_INDEX) {
         return true;
      }

      // By default, if there is an error, and the event listener has only
      // subscribed for the result, then we ignore partial results.
      return false;
   }

   private function isDiscardableResponse( event:Event,
                                           responseInfo:ResponseInfo ):Boolean {
      if (responseInfo.request == null) {
         // It's not a response style event (because there's no associated request).
         return false;
      }
      if (! this.isReflexiveOnly) {
         // If it's not a reflexive-only listener, it's interested in all events.
         return false;
      }
      if (responseInfo.requestor != _target) {
         // The request event wasn't raised by our target, and "reflexive-only"
         // restriction was specified, so we should indicate that the event is not
         // relevant for this handler.
         return true;
      }
      var isResponseStillApplicable:Boolean =
            this.pendingRequestRegistry.isResponseStillApplicable( event );
      if (! isResponseStillApplicable) {
         // 'reflexive'-only listeners are invoked only if the response is still
         // applicable (it may become inapplicable e.g., if the context of the
         // requestor object has changed since the time when request was made).
         return true;
      }
      return false;
   }

   /**
    * Returns the array of argument values for the invocation of the listener or handler,
    * given an event that potentially triggers the handler. Returns an empty array if the
    * event doesn't apply to the listener. Also returns empty if the response handler is a
    * property (as opposed to a method).
    */
   private function
   getListenerInvocationArgs( ev:Event, responseInfo:ResponseInfo ):Array/*?*/ {
      if (this.handlerInfo is TaggedPropertyInfo) {
         return [];
      }
      if (! this.isResponseHandler) {
         // The plain old [EventHandler] case: listener takes just the event itself as
         // the arg.
         return [ev];
      }
      // The [ResponseHandler] case: listener is a function that takes upto 4 args:
      // [response] or [request, result, error?, resultInfo?].

      var argCount:int = this.argTypes.length;
      if (argCount == RESPONSE_EVENT_ARG_INDEX + 1) {
         return [ev];
      }
      if (argCount == RESULT_ARG_INDEX + 1) {
         return [responseInfo.request, responseInfo.result];
      }
      if (argCount == ERROR_ARG_INDEX + 1) {
         return [responseInfo.request, responseInfo.result, responseInfo.error];
      }
      return [responseInfo.request, responseInfo.result, responseInfo.error,
              responseInfo.resultInfo];
   }

   private function get argTypes():Array/*Class*/ {
      var methodInfo:TaggedMethodInfo = this.handlerInfo as TaggedMethodInfo;
      if (methodInfo == null) {
         return [];
      }
      return methodInfo.argTypes;
   }
} // class

}